﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2014-02-16 11:30
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BassDrop.Core.Common.Cloud.Requests;
using BassDrop.Core.Common.General.Events;
using BassDrop.Core.Common.General.Threading;

namespace BassDrop.Core.Common.Cloud
{
	/// <summary>
	/// Provides centralized access to the connected cloud accounts in the system
	/// and asynchronous cloud request queues with automatic request merging, error handling
	/// and retrying on errors.
	/// </summary>
	public class CloudManager : IDisposable
	{
		private object m_lock = new object();
		private static CloudManager s_instance;

		/// <summary>
		/// Thread to run the request execution loop in the background
		/// </summary>
		private Task m_loopTask;

		/// <summary>
		/// Provides cancellation to the loop task
		/// </summary>
		private CancellationTokenSource m_cancelToken;

		/// <summary>
		/// Provides blocking function to the loop task while the queue is empty
		/// </summary>
		private ManualResetEvent m_waitHandle = new ManualResetEvent( false );

		/// <summary>
		/// Provides blocking function to the Join function while the loop is running
		/// </summary>
		private AsyncManualResetEvent m_joinHandle = new AsyncManualResetEvent();

		/// <summary>
		/// If true, the loop task exits when the queue runs out
		/// </summary>
		private volatile bool m_runout = false;

		/// <summary>
		/// The object is being disposed
		/// </summary>
		private volatile bool m_disposing = false;

		/// <summary>
		/// The request queue
		/// </summary>
		private List<Tuple<CloudRequestBase, CloudConnectionBase>> m_requests = new List<Tuple<CloudRequestBase, CloudConnectionBase>>();

		/// <summary>
		/// The path of the root directory of system-related files in the clouds
		/// </summary>
		public static string SystemRootDir
		{
			get
			{
				return "/System";
			}
		}

		/// <summary>
		/// The path of the root directory of the personal directories of the attached clients in the clouds
		/// </summary>
		public static string ClientsRootDir
		{
			get
			{
				return "/Clients";
			}
		}

		/// <summary>
		/// The path of the directory in the clouds where clients can leave various messages for each other
		/// </summary>
		public static string ClientMessagesDir
		{
			get
			{
				return SystemRootDir + "/ClientMessages";
			}
		}

		/// <summary>
		/// The path of the directory in the clouds where clients put their (vector-) clocks
		/// </summary>
		public static string ClocksDir
		{
			get
			{
				return ClientMessagesDir + "/Clocks";
			}
		}

		/// <summary>
		/// The path of the root directory of the elements in tme media library in the clouds
		/// </summary>
		public static string LibraryRootDir
		{
			get
			{
				return "/Library";
			}
		}

		/// <summary>
		/// The path of the directory in the clouds where the playlists in the library are stored
		/// </summary>
		public static string PlaylistsDir
		{
			get
			{
				return LibraryRootDir + "/Playlists";
			}
		}

		/// <summary>
		/// Returns the singleton instance of this class
		/// </summary>
		public static CloudManager Instance
		{
			get
			{
				return s_instance; // TODO: ha null, exception?
			}
		}

		/// <summary>
		/// The list of currently open cloud objects, calling Synchronize synchronizes them
		/// </summary>
		public HashSet<ISynchronizable> CloudObjects
		{
			get;
			private set;
		}

		private volatile bool m_running = true;

		/// <summary>
		/// Is a task currently running
		/// </summary>
		public bool Running
		{
			get
			{
				lock ( m_lock )
				{
					return m_running;
				}
			}
			set
			{
				lock ( m_lock )
				{
					if ( value != m_running )
					{
						m_running = value;

						if ( m_running )
						{
							OnRunningStarted();
						}
						else
						{
							OnRunningStopped();
						}
					}
				}
			}
		}

		#region Events

		/// <summary>
		/// Event fired when task processing starts
		/// </summary>
		public event EventHandler RunningStarted;

		/// <summary>
		/// Event fired when task processing stops
		/// </summary>
		public event EventHandler RunningStopped;

		/// <summary>
		/// Event fired when an error occurs while running the tasks
		/// </summary>
		public event EventHandler<CustomDataEventArgs<Exception>> ErrorOccured;

		/// <summary>
		/// Fires a RunningStarted event
		/// </summary>
		private void OnRunningStarted()
		{
			if ( RunningStarted != null )
			{
				RunningStarted( this, EventArgs.Empty );
			}
		}

		/// <summary>
		/// Fires a RunningStopped event
		/// </summary>
		private void OnRunningStopped()
		{
			if ( RunningStopped != null )
			{
				RunningStopped( this, EventArgs.Empty );
			}
		}

		/// <summary>
		/// Fires an ErrorOccured event
		/// </summary>
		/// <param name="e"></param>
		private void OnErrorOccured( Exception e )
		{
			if ( ErrorOccured != null )
			{
				ErrorOccured( this, new CustomDataEventArgs<Exception>( e ) );
			}
		}

		#endregion Events

		private CloudManager()
		{
			CloudObjects = new HashSet<ISynchronizable>();

			StartLoop();
		}

		/// <summary>
		/// Initializes the cloud manager
		/// </summary>
		public static void Init()
		{
			if ( s_instance == null )
			{
				s_instance = new CloudManager();
			}
			// TODO: else exception?
		}

		/// <summary>
		/// Destroys the instance
		/// </summary>
		/// <param name="force">stops the loop immediately</param>
		public static async Task Destroy( bool force )
		{
			if ( s_instance != null )
			{
				s_instance.m_disposing = true;

				if ( force )
				{
					s_instance.StopLoop();
				}
				else
				{
					s_instance.RunOut();
				}

				await s_instance.Join();
				s_instance.Dispose();
				s_instance = null;
			}
		}

		/// <summary>
		/// Starts executing the tasks from the queue
		/// </summary>
		public void StartLoop()
		{
			lock ( m_lock )
			{
				if ( m_loopTask == null )
				{
					m_cancelToken = new CancellationTokenSource();
					m_joinHandle.Reset();
					m_loopTask = Task.Factory.StartNew( async () =>
					{
						try
						{
							while ( true )
							{
								m_cancelToken.Token.ThrowIfCancellationRequested();
								if ( !await ExecuteNext() ) // TODO: párhuzamosan többet?
								{
									return;
								}
							}
						}
						catch ( Exception e )
						{
							if ( !(e is TaskCanceledException) )
							{
								OnErrorOccured( e );
							}

							throw;
						}
						finally
						{
							lock ( m_lock )
							{
								m_cancelToken.Dispose();
								m_cancelToken = null;
								m_loopTask = null;
								m_joinHandle.Set();
							}
						}
					},
					m_cancelToken.Token );
				}
			}
		}

		/// <summary>
		/// Immediately stops the task execution loop
		/// </summary>
		public void StopLoop()
		{
			lock ( m_lock )
			{
				if ( m_cancelToken != null )
				{
					m_cancelToken.Cancel();
					m_waitHandle.Set();

					Task.Factory.StartNew( async () =>
					{
						await Join();

						lock ( m_lock )
						{
							foreach ( var item in m_requests )
							{
								item.Item1.CanceledBySystem();
							}

							m_requests.Clear();
						}
					} );
				}
			}
		}

		/// <summary>
		/// Commands the loop to stop when the request queue empties
		/// </summary>
		/// <returns></returns>
		public void RunOut()
		{
			lock ( m_lock )
			{
				m_runout = true;
				m_waitHandle.Set();
			}
		}

		/// <summary>
		/// Waits until the loop stops
		/// </summary>
		public async Task Join()
		{
			await m_joinHandle.WaitAsync();
		}

		/// <summary>
		/// Schedules a new task to be executed
		/// </summary>
		/// <param name="request"></param>
		/// <param name="connection">The cloud account to execute the request on. If null is passed or left out, the default is used</param>
		public void Schedule( CloudRequestBase request, CloudConnectionBase connection = null )
		{
			lock ( m_lock )
			{
				if ( m_disposing )
				{
					throw new ObjectDisposedException( "CloudManager.Instance" );
				}

				foreach ( var item in m_requests )
				{
					if ( item.Item2 == connection && item.Item1.TryMerge( request ) )
					{
						// if we can merge it in somewhere, no need to add it
						return;
					}
				}

				m_requests.Add( new Tuple<CloudRequestBase, CloudConnectionBase>( request, connection ) );
				m_waitHandle.Set();
			}
		}

		/// <summary>
		/// Removes the passed request from the queue if it is in it
		/// </summary>
		/// <param name="request"></param>
		/// <param name="bySystem">the cancellation was requested by the system, not the user</param>
		public void Cancel( CloudRequestBase request, bool bySystem = false )
		{
			lock ( m_lock )
			{
				for ( int i = m_requests.Count - 1; i >= 0; i-- )
				{
					var item = m_requests[i].Item1;
					if ( item == request )
					{
						m_requests.RemoveAt( i );

						if ( bySystem )
						{
							item.CanceledBySystem();
						}
						else
						{
							item.CanceledByUser();
						}
					}
				}
			}
		}

		/// <summary>
		/// Schedules the synchronization of all attached cloud objects
		/// </summary>
		public void ScheduleSynchronize() // TODO: időnként automatikusan hívódjon meg
		{
			lock ( m_lock )
			{
				foreach ( var item in CloudObjects )
				{
					try
					{
						item.ScheduleSynchronize();
					}
					catch ( Exception e )
					{
						OnErrorOccured( e );
					}
				}
			}
		}

		/// <summary>
		/// Executes the next task from the queue. If it errors but can be repaired, reschedules it.
		/// </summary>
		/// <returns>False if the execution needs to stop</returns>
		private async Task<bool> ExecuteNext()
		{
			Tuple<CloudRequestBase, CloudConnectionBase> request = null;

			lock ( m_lock )
			{
				if ( m_requests.Count > 0 )
				{
					request = m_requests[0];
					m_requests.RemoveAt( 0 );
				}

				if ( m_requests.Count == 0 )
				{
					// If it was emptied, reset the wait handle, so that it will be locked in the next iteration
					// if no new item is added by that time
					m_waitHandle.Reset();
				}
			}

			if ( request != null )
			{
				if ( !await request.Item1.TryExecute( request.Item2 ?? Controller.Instance.PluginManager.GetDefaultCloudProvider() ) )
				{
					if ( (bool) request.Item1.CanRepairError )
					{
						Schedule( request.Item1, request.Item2 );
					}
					else
					{
						OnErrorOccured( request.Item1.Error );
					}
				}
			}
			else if ( m_runout )
			{
				return false;
			}
			else
			{
				// Wait for a new item to arrive
				Running = false;
				m_waitHandle.WaitOne();
				Running = true;
			}

			return true;
		}

		#region IDisposable Members

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			if ( m_cancelToken != null )
			{
				m_cancelToken.Dispose();
			}

			m_joinHandle.Set();
			m_waitHandle.Dispose();

			m_joinHandle.Set();
		}

		#endregion
	}
}
